/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * This class implements miscellaneous utility methods.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
public final class Misc {

    /**
     * No instance of this class.
     */
    private Misc() {
    } // end empty constructor

    /**
     * Converts an exception into a string.
     * @param exception exception to convert - should not be <tt>null</tt>
     * @return string representation of the passed exception
     */
    public static String convertException(final Value exception) {
        assert exception != null : "null exception";
        final Block exn = exception.asBlock();
        final StringBuilder sb = new StringBuilder();
        sb.append(exn.get(0).asBlock().get(0).asBlock().asString());
        if (exn.getWoSize() >= 2) {
            final int start;
            final Block bucket;
            if ((exn.getWoSize() == 2) && exn.get(1).isBlock()
                && (exn.get(1).asBlock().getTag() == 0)) {
                bucket = exn.get(1).asBlock();
                start = 0;
            } else {
                bucket = exn;
                start = 1;
            } // end if/else
            sb.append('(');
            final int sz = bucket.getWoSize();
            for (int i = start; i < sz; i++) {
                if (i > start) {
                    sb.append(", ");
                } // end if
                final Value v = bucket.get(i);
                if (v.isLong()) {
                    sb.append(v.asLong());
                } else if (v.isBlock()
                           && v.asBlock().getTag() == Block.STRING_TAG) {
                    sb.append('"');
                    sb.append(v.asBlock().asString());
                    sb.append('"');
                } else {
                    sb.append('_');
                } // end if/elsif/else
            } // end for
            sb.append(')');
        } // end if
        return sb.toString();
    } // end method 'convertException(Value)'

    /**
     * Converts a flag list into an integer. <br/>
     * The integer is the logical 'or' of the values of the flags in the list.
     * @param list flag identifier list, each flag identifier being an integer
     *             - should not be <tt>null</tt>
     * @param flags flags desription: <tt>flags[i]</tt> contains the integer
     *              value of the flag of identifier <tt>i</tt>
     *              - should not be <tt>null</tt>
     * @return the integer value corresponding to the flag list
     */
    public static int convertFlagList(final Value list, final int[] flags) {
        assert list != null : "null list";
        assert flags != null : "null flags";
        int res = 0;
        Value lst = list;
        while (lst != Value.EMPTY_LIST) {
            final Block b = lst.asBlock();
            res |= flags[b.get(0).asLong()];
            lst = b.get(1);
        } // end while
        return res;
    } // end method 'convertFlagList(Value, int[])'

    /**
     * Prepares arguments for system (shell-like execution). <br/>
     * Not very robust (sweet euphemism), as it just removes potential
     * (double) quotes enclosing an argument.
     * @param args arguments to prepare - should not be <tt>null</tt>
     * @return a copy of the array with prepared arguments
     */
    public static String[] prepareArguments(final String[] args) {
        assert args != null : "null args";
        final int len = args.length;
        final String[] res = new String[len];
        for (int i = 0; i < len; i++) {
            final String s = args[i];
            final char first = s.charAt(0);
            final int lastIndex = s.length() - 1;
            res[i] = (lastIndex >= 0)
                && ((first == '\'') || (first == '\"'))
                && (first == s.charAt(lastIndex))
                ? s.substring(1, lastIndex)
                : s;
        } // end for
        return res;
    } // end method 'prepareArguments(String[])'

    /** State definition for parser state. */
    private static enum ParserState { NORMAL, SINGLE_QUOTE, DOUBLE_QUOTE, ESCAPED };

   /**
    * Parses the passed command line into a list of strings.
    * @param s string to parse - should not be <tt>null</tt>
    * @return the parsed string, as a list of strings
    */
    public static List<String> parseCommandLine(final String s) {
        assert s != null : "null s";
        final List<String> res = new LinkedList<String>();
        StringBuilder token = new StringBuilder();
        ParserState state = Misc.ParserState.NORMAL;
        final int len = s.length();
        for (int i = 0; i < len; i++) {
            final char ch = s.charAt(i);
            switch (state) {
            case NORMAL:
                switch (ch) {
                case ' ':
                    if (token.length() > 0) {
                        res.add(token.toString());
                    } // end if
                    token = new StringBuilder();
                    break;
                case '\\':
                    state = Misc.ParserState.ESCAPED;
                    break;
                case '\'':
                    state = Misc.ParserState.SINGLE_QUOTE;
                    break;
                case '"':
                    state = Misc.ParserState.DOUBLE_QUOTE;
                    break;
                default:
                    token.append(ch);
                    break;
                } // end switch
                break;
            case SINGLE_QUOTE:
                if (ch == '\'') {
                    state = Misc.ParserState.NORMAL;
                } else {
                    token.append(ch);
                } // end if/else
                break;
            case DOUBLE_QUOTE:
                if (ch == '"') {
                    state = Misc.ParserState.NORMAL;
                } else {
                    token.append(ch);
                } // end if/else
                break;
            case ESCAPED:
                state = Misc.ParserState.NORMAL;
                token.append(ch);
                break;
            default:
                assert false : "invalid state";
            } // end switch
        } // end for
        if (token.length() > 0) {
            res.add(token.toString());
        } // end if
        return res;
    } // end method 'parseCommandLine(String)'

    /**
     * Tests if the underlying platform is a Windows flavour. <br/>
     * Not really reliable, but cannot really be in pure Java.
     * @return <tt>true</tt> if the underlying platform is a Windows flavour,
     *         <tt>false</tt> otherwise
     */
    public static boolean isWindowPlatform() {
        try {
            final String os = System.getProperty("os.name");
            return (os != null) && (os.toLowerCase().indexOf("windows") > -1);
        } catch (final Throwable t) {
            return false;
        } // end try/catch
    } // end method 'isWindowPlatform()'

    /**
     * Tests if the underlying platform is a big-endian one.
     * @return <tt>true</tt> if the underlying platform is a big-endian one,
     *         <tt>false</tt> otherwise
     */
    public static boolean isBigEndianPlatform() {
        return ByteOrder.BIG_ENDIAN.equals(ByteOrder.nativeOrder());
    } // end method 'isBigEndianPlatform()'

    /**
     * Expands command-line arguments, that is resolves arguments with
     * wildcards characters.
     * @param directory current directory for wildcard resolution
     *                  - should not be <tt>null</tt> and should also be
     *                  an existing directory
     * @param args arguments to expand - should not be <tt>null</tt>
     * @return the expanded arguments
     * @throws IOException if an error occurs during expansion
     */
    public static String[] expandCommandLine(final File directory,
                                             final String[] args)
        throws IOException {
        assert directory != null : "null directory";
        assert directory.exists() && directory.isDirectory()
            : "directory should be an existing directory";
        assert args != null : "null args";
        final List<String> tmp = new LinkedList<String>();
        for (String a : args) {
            expandArgument(directory, a, tmp);
        } // end for
        final String[] res = new String[tmp.size()];
        return tmp.toArray(res);
    } // end method 'expandCommandLine(File, String[])'

    /**
     * Expands an argument.
     * @param dir directory to perform expansion into
     *            - should not be <tt>null</tt>
     * @param arg argument - should not be <tt>null</tt>
     * @param list where to add expansion result - should not be <tt>null</tt>
     * @throws IOException if an error occurs during expansion
     */
    private static void expandArgument(final File dir,
                                       final String arg,
                                       final List<String> list)
        throws IOException {
        assert dir != null : "null dir";
        assert dir.exists() && dir.isDirectory()
            : "dir should be an existing directory";
        assert arg != null : "null arg";
        assert list != null : "null list";
        if (arg.charAt(0) == '@') {
            expandDiversion(dir, arg.substring(1), list);
        } else if ((arg.indexOf('*') > -1) || (arg.indexOf('?') > -1)) {
            expandPattern(dir, arg, list);
        } else {
            list.add(arg);
        } // end if/elsif/else
    } // end method 'expandArgument(File, String, List<String>)'

    /**
     * Expands a file pattern (not very robust).
     * @param dir directory to perform expansion into
     *            - should not be <tt>null</tt>
     * @param patt file pattern - should not be <tt>null</tt>
     * @param list where to add expansion result - should not be <tt>null</tt>
     * @throws IOException if an error occurs during expansion
     */
    private static void expandPattern(final File dir,
                                      final String patt,
                                      final List<String> list)
        throws IOException {
        assert dir != null : "null dir";
        assert dir.exists() && dir.isDirectory()
            : "dir should be an existing directory";
        assert patt != null : "null patt";
        assert list != null : "null list";
        try {
            final Pattern p = compilePattern(patt);
            for (String file : dir.list()) {
                if (p.matcher(file).matches()) {
                    list.add(file);
                } // end if
            } // end for
        } catch (final PatternSyntaxException pse) {
            list.add(patt);
        } // end try/catch
    } // end method 'expandPattern(File, String, List<String>)'

    /**
     * Expands a diversion/responsefile.
     * @param dir directory to perform expansion into
     *            - should not be <tt>null</tt>
     * @param arg argument that is file name - should not be <tt>null</tt>
     * @param list where to add expansion result - should not be <tt>null</tt>
     * @throws IOException if an error occurs during expansion
     */
    private static void expandDiversion(final File dir,
                                        final String arg,
                                        final List<String> list)
        throws IOException {
        assert dir != null : "null dir";
        assert dir.exists() && dir.isDirectory()
            : "dir should be an existing directory";
        assert arg != null : "null arg";
        assert list != null : "null list";
        final File f = new File(dir, arg);
        final InputStream in = new FileInputStream(f);
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int b = in.read();
        while (b != -1) {
            baos.write(b);
            b = in.read();
        } // end while
        in.close();
        final String s =
            fr.x9c.cadmium.util.Misc.convertBytesToString(baos.toByteArray());
        baos.close();
        final int len = s.length();
        int i = 0;
        while (i < len) {
            while ((i < len) && (Character.isSpaceChar(s.charAt(i)))) i++;
            if (i >= len) break;
            final int start = i;
            boolean inQuote = false;
            final StringBuilder tmp = new StringBuilder();
            while (i < len) {
                if (!inQuote) {
                    if (Character.isSpaceChar(s.charAt(i))) break;
                    if (s.charAt(i) == '"') { inQuote = true; i++; continue; }
                    tmp.append(s.charAt(i++));
                } else {
                    switch (s.charAt(i)) {
                    case '"':
                        inQuote = false;
                        i++;
                        continue;
                    case '\\':
                        if ((i + 4 <= len)
                            && (s.charAt(i) == '\\')
                            && (s.charAt(i + 1) == '\\')
                            && (s.charAt(i + 2) == '\\')
                            && (s.charAt(i + 3) == '"')) {
                            i += 4;
                            tmp.append('\\');
                            tmp.append('"');
                            continue;
                        } // end if
                        if ((i + 3 <= len)
                            && (s.charAt(i) == '\\')
                            && (s.charAt(i + 1) == '\\')
                            && (s.charAt(i + 2) == '"')) {
                            i += 3;
                            tmp.append('\\');
                            inQuote = false;
                            continue;
                        } // end if
                        if ((i + 2 <= len)
                            && (s.charAt(i + 1) == '"')) {
                            i += 2;
                            tmp.append('"');
                            continue;
                        } // end if
                        tmp.append(s.charAt(i++));
                        break;
                    default:
                        tmp.append(s.charAt(i++));
                        break;
                    } // end switch
                } // end if/else
            } // end while
            expandArgument(dir, tmp.toString(), list);
            i++;
        } // end while
    } // end method 'expandDiversion(File, String, List<String>)'

    /**
     * Compiles a string into a pattern, handling wildcard characters.
     * @param s string to compile - should not be <tt>null</tt>
     * @return a pattern corresponding to the passed string
     * @throws PatternSyntaxException if an error occurs while trying to
     *                                compile pattern
     */
    private static Pattern compilePattern(final String s)
        throws PatternSyntaxException {
        assert s != null : "null s";
        final int len = s.length();
        final StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
            final char c = s.charAt(i);
            switch (c) {
            case '*':
                sb.append(".*");
                break;
            case '?':
                sb.append(".");
                break;
            case '.':
                sb.append("\\.");
                break;
            default:
                sb.append(c);
                break;
            } // end switch
        } // end for
        return Pattern.compile(sb.toString());
    } // end method 'compilePattern(String)'

} // end class 'Misc'
